# Copyright (c) 2009 Peter Houghton 
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

# WATIR GRID

require 'rubygems'
require 'yaml'
require 'webrick'
require 'drb'


module WatirGrid
  
  # Waiting time for retrys
  MAX_WAITING_TIME=4
  SERVER_BASE="gridhubserver"
  
  class WatirServerDetail
    attr_accessor :browser_key , :address , :port , :description , :name , :locked
    
    # Create serialised yaml version of the object
    def to_yaml
      obj_hash = Hash.new
      obj_details_hash=Hash.new
      
      obj_details_hash["browser_key"]=self.browser_key
      obj_details_hash["address"]=self.address
      obj_details_hash["port"]=self.port
      obj_details_hash["description"]=self.description
      obj_details_hash["locked"]=self.locked
      
      obj_hash[self.name]=obj_details_hash
      return YAML::dump(obj_hash)
    end # method
  end # class

  class GridHub
  
    attr_accessor :servers
  
    def initialize(debug=false)
      self.servers =Hash.new
      @debug=debug
    end # init
  
    # Read in the YAML file containing WATIR Server details
    # Watir Servers have:
    # 'name'  is a label for a given set of config, these shouls be unique
    # 'browser_key'  is a tag, used to identify an browser/OS combo e.g.: ie7_xp 
    #                 you may have several identical instances of this.
    # 'port'  is the port on which to access the Watir Server
    # 'address'  is the ip address of the Watir Server
    # 'description'  is the human readable description of the server.
    # 
    def load_server_config(file_path)
      loaded_servers= YAML::load(File.open(file_path))
      loaded_servers.each do |server_name,server_values| 
        putsd server_name
        wserver = WatirServerDetail.new
        wserver.name=         server_name
        wserver.browser_key=  server_values["browser_key"]
        wserver.address=           server_values["address"]
        wserver.port=         server_values["port"]
        wserver.description=  server_values["description"]
        
        # Output debug of each server entry
        putsd "browser_key: #{wserver.browser_key}"
        putsd "address: #{wserver.address}"
        putsd "port: #{wserver.port}"
        putsd "description: #{wserver.description}"
        
        if  wserver.browser_key==nil || wserver.address==nil || wserver.port==nil || wserver.description==nil
          
          raise "Missing fields in Server Configuration file: #{file_path}"
        end # end
      
        # By default a server is unlocked, and ready to be used
        wserver.locked=false
      
        # Check if that type of browser exists in store
        if self.servers.has_key? wserver.browser_key
          # then add to existing list
          self.servers[wserver.browser_key].push wserver
        else 
          # if not in store, add a new array for this type of browser
          self.servers[wserver.browser_key] = Array.new
          self.servers[wserver.browser_key].push wserver
        end # else
      
      end # each kv
    end # method
  
    # Returns the number of unlocked servers, for a given browser_key_name
    #
    def free_servers(browser_key_name)
      return find_free_servers(browser_key_name).length
    end # method
  
    # Finds and locks a Watir Server, that is configured as having browser_key_name
    # Returns a YAML response
    # The 'server_status' 'locked' field indicates whether server has been assigned.
    #  
    def get_server(browser_key_name)
      server_status = Hash.new # YAML response object
      server_status["browser_key"]=browser_key_name
    
      # Does this browser exist in the servers list?
      if self.servers.has_key? browser_key_name
        server_status["exists"]=true
        unless free_servers(browser_key_name)==0
          chosen_server = find_free_servers(browser_key_name)[0] # Take first free server
          chosen_server.locked=true

          server_status["name"]=chosen_server.name
          server_status["locked"]=chosen_server.locked
          server_status["address"]=chosen_server.address
          server_status["port"]=chosen_server.port
          
      
        else
          server_status["name"]=browser_key_name
          server_status["locked"]=false
        end # else
    
      else # This does not exist, so send back that info to client
        server_status["name"]=browser_key_name
        server_status["locked"]=false
        server_status["exists"]=false
      end # if 
    
      return YAML::dump({"grid_response"=>server_status})
    end # method
  
    # Release the lock on a given watir server
    # Returns YAML response
    # If server does not exist, then exists 'false' is returned
    # Otherwise exists 'true' is returned.
    #
    def release_server(watir_server_name)
      server_status = Hash.new # YAML response object
      server_status["name"]=watir_server_name
      server_status["exists"]=false
      
      # List of all servers, geerated from adding all lists of servers together 
      all_servers=Array.new
      self.servers.values.each { |server_array| all_servers.concat server_array  }

      # Work through list, looking for desired server
      all_servers.each do |watir_server|
          
        if watir_server.name==watir_server_name
          # Found the server, Set the watir server to be unlocked ie: released
          # This works as its all pass by reference 
          watir_server.locked=false
          server_status["exists"]=true
          server_status["locked"]=watir_server.locked
          break
        end # if
      end # end self
      
      return YAML::dump({"grid_response"=>server_status})
      
    end # method
  
    private
   
    # Get the free/unlocked servers 
    # Only returns servers that match the browser_key_name
    #
    def find_free_servers(browser_key_name)
      free_wservers=Array.new
    
      self.servers[browser_key_name].each do |wserver| 
        if wserver.locked==false
          free_wservers.push wserver
        end # else
      end # each
    
      return free_wservers
    end # method
  
    # Debug output to command line
    #
    def putsd(textmsg)
      if @debug 
        puts textmsg
      end # if
    end # method
  end # end class


  # The HTTP server that opens up the hub, to network/HTTP access.
  # Once instance of this per grid.
  #
  class GridHubServer
      
      attr_accessor :debug
      
      def initialize(config , port=7777)
        @config_file=config
        @assigned_port=port
      end # method

      # Debug output method
      # GridHubServer#debug=true  to enable
      #
      def putsd(textmsg)
        if self.debug 
          puts "GridHubServer:"
          puts textmsg
        end # if
      end # method


      # Start services
      # Mounts all URLs associated with the server.
      # assigns the code blocks for the handling of the HTTP / YAML communication
      # Relays calls to a GridHub instance
      #
      def start
        @web_server = WEBrick::HTTPServer.new( :Port => @assigned_port )  
        @grid_server = GridHub.new
        @grid_server.load_server_config(@config_file)
        
        # server_list
        # Returns a list of all the servers configured
        #
        @web_server.mount_proc("/#{SERVER_BASE}/server_list") do |req, response|
          the_yaml_output = ""
          @grid_server.servers.each_value do |a_server_list|
            a_server_list.each do |a_server|            
               the_yaml_output << a_server.to_yaml
            end # each
          end # each_value
          putsd "Server_list:\n" + the_yaml_output
          
          response.body=the_yaml_output
          response['Content-Type'] = "text/yaml"
        end # block

        
        # get_server
        # if available, locks an instance of the requested server.
        #
        @web_server.mount_proc("/#{SERVER_BASE}/get_server") do |req, response|
          the_yaml_output = ""
          browser_name = req.path.match(/(\w+$)/)[0]
          the_yaml_output=@grid_server.get_server(browser_name)
          putsd "Get_server:\n" + the_yaml_output
          response.body=the_yaml_output
          response['Content-Type'] = "text/yaml"
        end # block
 
        # release_server
        # if found, release the server
        #
        @web_server.mount_proc("/#{SERVER_BASE}/release_server") do |req, response|
          the_yaml_output = ""
          watir_server_name = req.path.match(/(\w+$)/)[0]
          the_yaml_output=@grid_server.release_server(watir_server_name)
          putsd "Release_server:\n" + the_yaml_output
          response.body=the_yaml_output
          response['Content-Type'] = "text/yaml"
        end # block
        
        trap("INT"){ @web_server.shutdown }
        @web_server.start
        
      end # method
      
      # Shutdown the web server
      def stop
        @web_server.shutdown
      end # method
  end # end class


  # The Client side software
  # This is used in your test/rspec code
  #
  class GridClient

    def initialize()
      @wserver_name=""
      @wserver_host=nil
      @wserver_port=nil
      @waiting=false    
    end # init

    # Passed browser_key e.g.: "ie6_xp" , the grid hub host and port name.
    # Get Browser returns drb obj
    #
    def get_browser(browser_key_name , host , port)

      # the details of the Watir Grid Hub, have been passed in
      @hub_host=host
      @hub_port=port
      
      # Lock
	    response = Net::HTTP.start(@hub_host, @hub_port) do |http|
	      http.get("/#{SERVER_BASE}/get_server/#{browser_key_name}")
	    end # http
      response_hash = YAML::load(response.body)["grid_response"]

      if response_hash["exists"] || response_hash["exists"]==nil
        if response_hash["locked"] || response_hash["locked"]==nil
          @wserver_name = response_hash["name"]
          @wserver_host = response_hash["address"]
          @wserver_port = response_hash["port"]
          DRb.start_service
          return DRbObject.new(nil, "druby://#{@wserver_host}:#{@wserver_port}")
        else 
          wait_n_retry do
            get_browser(browser_key_name, @hub_host , @hub_port)
          end # wait
        end # else not locked
        
      else # Server does not exist!
        raise "Watir Server key named: #{browser_key_name} not found."
      end # else 

    end # method
    
    # Release lock on current server, so it re-enters the pool
    #
    def release_browser
      response = Net::HTTP.start(@hub_host, @hub_port) do |http|
	      http.get("/#{SERVER_BASE}/release_server/#{@wserver_name}")
	    end # http
      response_hash = YAML::load(response.body)["grid_response"]
	    
      unless response_hash["exists"]==true && response_hash["locked"]==false
	      raise "Unable to release server: #{@wserver_name} Exists:#{response_hash['exists']} Locked#{response_hash['locked']}"
	    end # unless
	    
    end # method
    
    
    # Wait for a while and retry 
    def wait_n_retry
      rand MAX_WAITING_TIME
      yield
    end #
  
  end # class

end # end module
